/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                                                                       *
 *   JavaWorld Library, Copyright 2011 Bryan Chadwick                    *
 *                                                                       *
 *   FILE: ./hidden/SnakeGame.java                                       *
 *                                                                       *
 *   This file is part of JavaWorld.                                     *
 *                                                                       *
 *   JavaWorld is free software: you can redistribute it and/or          *
 *   modify it under the terms of the GNU General Public License         *
 *   as published by the Free Software Foundation, either version        *
 *   3 of the License, or (at your option) any later version.            *
 *                                                                       *
 *   JavaWorld is distributed in the hope that it will be useful,        *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of      *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the       *
 *   GNU General Public License for more details.                        *
 *                                                                       *
 *   You should have received a copy of the GNU General Public License   *
 *   along with JavaWorld.  If not, see <http://www.gnu.org/licenses/>.  *
 *                                                                       *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

package hidden;

import image.*;
import world.World;

/** Snake Game demo */
public class SnakeGame extends World{
    public static void main(String[] s){
        new SnakeGame(new Consts()).bigBang();
    }
        
    Consts consts;
    LoSeg segs;
    Food food;
    String dir;
    
    SnakeGame(Consts cs){
        this(cs, cs.initialSegs, cs.randomFood(), "left");    
    }
    SnakeGame(Consts cs, LoSeg segs, Food food, String dir){
        this.consts = cs;
        this.segs = segs;
        this.food = food;
        this.dir = dir;
    }
    
    public Scene onDraw(){
        return segs.draw(consts,
                food.draw(consts,
                        new EmptyScene(consts.WIDTH_PIXELS, consts.HEIGHT_PIXELS)));
    }
    public double tickRate(){ return 0.2; }
    public SnakeGame onTick(){
        if(isEating())return grow();
        else return move();
    }
    public SnakeGame onKey(String ke){
        if(ke.equals("up") || ke.equals("down") ||
           ke.equals("left") || ke.equals("right"))
            return new SnakeGame(this.consts, this.segs, this.food, ke);
        else return this;
    }
    public boolean stopWhen(){ return isGameOver(); }
    public Scene lastScene(){
        return onDraw().placeImage(new Text("Game Over", 40, "gray").overlayxy(
                                       new Text("Game Over", 40, "blue"), -2,-2),
                                       consts.WIDTH_PIXELS/2, consts.WIDTH_PIXELS/2);    
    }
    
    SnakeGame move(){
        return new SnakeGame(consts,
                new CLoSeg(segs.first().move(dir), segs.dropLast()),
                food, dir);
    }
    SnakeGame grow(){
        return new SnakeGame(consts,
                new CLoSeg(segs.first().move(dir), segs),
                consts.randomFood(), dir);
    }
    boolean isEating(){
        return segs.first().same(food);
    }
    boolean isGameOver(){
        return (segs.rest().contains(segs.first()) ||
                !segs.first().onscreen(consts.WIDTH_CELLS, consts.HEIGHT_CELLS));
    }
}

class Consts{
    int CELL_SIZE = 20;
    int WIDTH_CELLS = 20;
    int HEIGHT_CELLS = 20;
    int WIDTH_PIXELS = CELL_SIZE*WIDTH_CELLS;
    int HEIGHT_PIXELS = CELL_SIZE*HEIGHT_CELLS;
        
    LoSeg initialSegs = new CLoSeg(new Seg(WIDTH_CELLS/2, HEIGHT_CELLS/2),
            new CLoSeg(new Seg(WIDTH_CELLS/2+1, HEIGHT_CELLS/2), new ELoSeg()));
    
    Food randomFood(){
        return new Food((int)(Math.random()*WIDTH_CELLS),
                        (int)(Math.random()*HEIGHT_CELLS));
    }
}

/** Represents a List of Segments */
abstract class LoSeg{
    abstract boolean isEmpty();
    abstract LoSeg dropLast();
    abstract Seg first();
    abstract LoSeg rest();
    abstract Scene draw(Consts cs, Scene scn);
    abstract boolean contains(Seg s);
}
/** Represents an empty List of Segments */
class ELoSeg extends LoSeg{
    ELoSeg(){}
    boolean isEmpty(){ return true; }
    Seg first(){ throw new RuntimeException("No First"); }
    LoSeg rest(){ throw new RuntimeException("No Rest"); }
    LoSeg dropLast(){ throw new RuntimeException("No Last to Drop"); }
    Scene draw(Consts cs, Scene scn){ return scn; }
    boolean contains(Seg s){ return false; }
}
/** Represents a non-empty List of Segments */
class CLoSeg extends LoSeg{
    Seg first;
    LoSeg rest;
    /** Construct a Cons LoB (with a first and rest) */
    CLoSeg(Seg first, LoSeg rest){
        this.first = first;
        this.rest = rest;
    }
    boolean isEmpty(){ return false; }
    Seg first(){ return first; }
    LoSeg rest(){ return rest; }
    LoSeg dropLast(){
        if(rest.isEmpty())return rest;
        else return new CLoSeg(first, rest.dropLast());
    }
    Scene draw(Consts cs, Scene scn){
        return first.draw(cs, rest.draw(cs, scn));
    }
    boolean contains(Seg s){ return first.same(s) || rest.contains(s); }
}

class Seg{
    int x;
    int y;
    Seg(int x, int y){
        this.x = x;
        this.y = y;
    }
    Seg move(String dir){
        if(dir.equals("up"))return new Seg(x,y+1);
        else if(dir.equals("down"))return new Seg(x,y-1);
        else if(dir.equals("left"))return new Seg(x-1,y);
        else if(dir.equals("right"))return new Seg(x+1,y);
        else throw new RuntimeException("Bad Direction: "+dir);
    }
    int cell2pixelsX(Consts cs){
        return x*cs.CELL_SIZE+cs.CELL_SIZE/2;
    }
    int cell2pixelsY(Consts cs){
        return cs.HEIGHT_PIXELS-y*cs.CELL_SIZE-cs.CELL_SIZE/2;
    }
    Scene draw(Consts cs, Scene scn){
        return scn.placeImage(new Circle(cs.CELL_SIZE/2, "solid", this.color())
                                    .overlay(new Circle(cs.CELL_SIZE/2, "outline", "black")),
                    cell2pixelsX(cs), cell2pixelsY(cs));
    }
    String color(){ return "red"; }
    
    boolean same(Seg s){
        return (s.x == this.x && s.y == this.y);
    }
    boolean onscreen(int w, int h){
        return ((0 <= x && x < w) &&
                (0 <= y && y < h));
    }
}
class Food extends Seg{
    Food(int x, int y){ super(x,y); }

    String color(){ return "green"; }
}
